# pita: An invisible audio player
# Copyright (C) 2006-2007 Austin Bingham
#
# This program is free software# you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation# either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY# without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program# if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# You can reach the author at:
#     austin.bingham@gmail.com

'''
The idea here is to combine the parsing of command-line options
and config-file into a single system. You can define a single
configurable option with both its command-line argument and
its config-file location and other metadata (e.g. help string)
in one place, and the config parser will take care of looking
for values in all the right places.
'''

import ConfigParser, optparse, sys

class OptionBase:
    '''
    Base of all options. Just defines the basic interface and
    creates storage for data.
    '''
    def __init__(self,
                 name,
                 short = None,
                 long = None,
                 location = None,
                 default = None,
                 type = str,
                 help = None):
        self.name = name
        self.short = short
        self.long = long
        self.location = location
        self.default = default
        self.type = type
        self.help = help

    def add_to_parser(self, parser):
        '''
        This is called to let the Option configure an OptionParser object
        '''
        pass

class Option(OptionBase):
    '''
    This is the basic Option type, used for most string and numeric option
    types. In fact, this will generally work for types that can be constructed
    from strings. This class adds its option to the OptionParser using a basic
    'store' action.
    '''
    def __init__(self,
                 name,
                 short = None,
                 long = None,
                 location = None,
                 default = None,
                 type = str,
                 help = None):
        OptionBase.__init__(self,
                            name,
                            short,
                            long,
                            location,
                            default,
                            type,
                            help)

    def add_to_parser(self, parser):
        parser.add_option(self.short,
                          self.long,
                          dest = self.name,
                          action = 'store',
                          help = self.help)

class BoolOption(OptionBase):
    '''
    This Option type should be used for boolean options that default to
    one boolean value or the other. 
    '''
    def __init__(self,
                 name,
                 short = None,
                 long = None,
                 location = None,
                 default_true = False,
                 help = None):
        self.default_true = default_true

        OptionBase.__init__(self,
                            name,
                            short,
                            long,
                            location,
                            default_true == True,
                            type = bool,
                            help=help)
        self.default_true = default_true

    def add_to_parser(self, parser):
        if self.default_true:
            action = 'store_false'
        else:
            action = 'store_true'
        
        parser.add_option(self.short,
                          self.long,
                          dest = self.name,
                          action = action)
                          
class Config(ConfigParser.ConfigParser):
    '''
    You populate a Config object with a set of Options which
    describe the configurable options that the Config should
    work with. For each logical option, call add_option with
    an Option object. After all of the options have been
    added, call read() and the Config object will take care
    of determining the configured value for all of your
    Options.

    Basic usage is as follows:
    - call add_option() once for each configurable option
    - call read() with the appropriate command-line options
      and a list of config-files.
    - access configured values using Config.<option-name>...the values
      are added to the Config object as attributes.
    '''
    def __init__(self):
        ConfigParser.ConfigParser.__init__(self)
        self._options = {}

    def add_option(self, option):
        '''
        Adds a configurable option to the Config object. 'options'
        should be of type Option.
        '''
        if self._options.has_key(option.name):
            raise 'Duplicate option name used: %s' % option.name
        self._options[option.name] = option

    def read(self,
             args = sys.argv[1:],
             config_files = [],
             usage=None):
        '''
        This actually reads config files and command-line arguments
        to determine the configured values for the options in this
        Config object. Command-line arguments take precedence over
        config-file values.

        args: the command-line arguments to parse
        
        config_files: The config files to parse for configuration
        info
        
        usage: The usage string for this Config. This is used if
        help is requested.            
        '''

        # first, parse the command line arguments, storing them
        # in a standard OptionParser output
        parser = optparse.OptionParser(usage=usage)
        for opt in self._options.itervalues():
            opt.add_to_parser(parser)

        (options, args) = parser.parse_args(args)

        # next, parse the config files using our base class
        ConfigParser.ConfigParser.read(self, config_files)

        # now, for each options, we have to check the values from both the command-line and
        # config-file parsers. If the command-line parser reports a non-default value, then
        # that's what we use. Otherwise, we look to see if the option was configured in a
        # config file; if so, that value is used. If it's not configured in either place,
        # then the default is used.
        for opt in self._options.itervalues():
            # if nothing else, just use the default value for the option
            val = opt.default

            # See if command-line for option was specified
            str_val = getattr(options, opt.name)
            if not str_val is None:
                val = opt.type(val)

            # ...otherwise, see if it's set in the config-files...
            elif self.has_option(opt.location[0],
                                 opt.location[1]):
                val = opt.type(self.get(opt.location[0],
                                        opt.location[1]))

            # ...finally, set the value as an attr on this object
            setattr(self, opt.name, val)

        return args
